/**
 * This file is Copyright © 2008 Software Craftsmen Limited. All Rights Reserved.
 */
package com.softwarecraftsmen.orogeny.actions.zip.signing;

import static com.softwarecraftsmen.archivers.zip.ZipArchiver.ZipArchive;
import com.softwarecraftsmen.archivers.zip.zipEntryAndData.ByteBackedZipEntryAndData;
import com.softwarecraftsmen.archivers.zip.zipEntryAndData.ExistingZipEntryBackedZipEntryAndData;
import com.softwarecraftsmen.archivers.zip.zipEntryAndData.ZipEntryAndData;
import com.softwarecraftsmen.orogeny.actions.keys.ConvenientKey;
import com.softwarecraftsmen.orogeny.actions.manifest.ManifestAttributesManipulator;
import static com.softwarecraftsmen.orogeny.actions.manifest.ManifestAttributesManipulator.basicManifest;
import com.softwarecraftsmen.orogeny.actions.manifest.ManifestBackedZipEntryAndData;
import com.softwarecraftsmen.orogeny.actions.zip.ZipEntryFilter;
import com.softwarecraftsmen.orogeny.buildEnvironments.BuildEnvironment;
import com.softwarecraftsmen.orogeny.buildLogs.BuildLog;
import static com.softwarecraftsmen.orogeny.buildLogs.Verbosity.Summary;
import com.softwarecraftsmen.orogeny.filing.AbsoluteFile;
import static com.softwarecraftsmen.orogeny.filing.AbsoluteFile.createTemporaryFile;
import com.softwarecraftsmen.orogeny.filing.absolutePathMetaData.AbsolutePathMetaData;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import static java.lang.String.format;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.LinkedHashSet;
import static java.util.Locale.UK;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;

public class JarSigner
{
	private final BuildLog buildLog;
	private final BuildEnvironment buildEnvironment;

	public JarSigner(final @NotNull BuildLog buildLog, final @NotNull BuildEnvironment buildEnvironment)
	{
		this.buildLog = buildLog;
		this.buildEnvironment = buildEnvironment;
	}

	public void signJar(final @NotNull AbsoluteFile jarFile, final @NotNull ConvenientKey convenientKey, final @NotNull PrivateKey privateKey, final @NotNull X509Certificate... signerCertificateChain)
	{
		buildLog.writeMessage(Summary, format(UK, "About to sign jar %1$s", jarFile));

		final AbsolutePathMetaData originalMetaData = jarFile.dynamicMetaData().fix();
		final long modificationTimeInMillisecondsSinceEpoch = originalMetaData.modificationTimeInMillisecondsSince1970();

		boolean exceptionThrown = true;
		final JarFile jar = jarFile.openAsJarFile();
		final AbsoluteFile temporaryJarFile;
		try
		{
			temporaryJarFile = createTemporaryFile("signed.jar");
			final JarFileHelper jarFileHelper = new JarFileHelper(jar);
			final Set<ZipEntryAndData> entries = new AddMetaDataToZipEntries(jarFileHelper, modificationTimeInMillisecondsSinceEpoch, convenientKey).allZipEntriesWithMetaData(privateKey, signerCertificateChain);
			ZipArchive.archive(temporaryJarFile, entries);
			exceptionThrown = false;
		}
		catch (IOException e)
		{
			throw new IllegalStateException(e);
		}
		finally
		{
			try
			{
				jar.close();
			}
			catch (IOException e)
			{
				if (!exceptionThrown)
				{
					//noinspection ThrowFromFinallyBlock
					throw new IllegalStateException(e);
				}
			}
		}
		final AbsoluteFile backUp = jarFile.moveToTimestampedName();
		try
		{
			temporaryJarFile.hardLinkTo(jarFile);
			temporaryJarFile.setMetaData(originalMetaData);
		}
		catch (IOException e)
		{
			throw new IllegalStateException(format(UK, "Could not copy %1$s to %2$s", temporaryJarFile, jarFile), e);
		}
		backUp.delete();
	}

	private class AddMetaDataToZipEntries
	{
		private final JarFileHelper jarFileHelper;
		private final long modificationTimeInMillisecondsSinceEpoch;
		private final ConvenientKey convenientKey;

		public AddMetaDataToZipEntries(final @NotNull JarFileHelper jarFileHelper, final long modificationTimeInMillisecondsSinceEpoch, final ConvenientKey convenientKey)
		{
			this.jarFileHelper = jarFileHelper;
			this.modificationTimeInMillisecondsSinceEpoch = modificationTimeInMillisecondsSinceEpoch;
			this.convenientKey = convenientKey;
		}

		@NotNull
		public Set<ZipEntryAndData> allZipEntriesWithMetaData(final @NotNull PrivateKey privateKey, final @NotNull X509Certificate... signerCertificateChain)
		{
			final Set<ZipEntryAndData> allEntries = jarFileHelper.allUsefulEntries();
			createSignatureBlockFile(allEntries, privateKey, createSignatureFile(allEntries, createManifestWithDigests(allEntries)), signerCertificateChain);
			return allEntries;
		}

		private Manifest createManifestWithDigests(final Set<ZipEntryAndData> allEntries)
		{
			final Manifest manifestWithDigests = jarFileHelper.manifestWithDigests(allEntries);
			allEntries.add(new ManifestBackedZipEntryAndData(manifestWithDigests, modificationTimeInMillisecondsSinceEpoch));
			return manifestWithDigests;
		}

		private byte[] createSignatureFile(final Set<ZipEntryAndData> allEntries, final Manifest manifestWithDigests)
		{
			final SignatureFile signatureFile = SignatureFile.sha1SignatureFile(manifestWithDigests);
			return signatureFile.addToEntriesAndReturnContent(allEntries, modificationTimeInMillisecondsSinceEpoch, convenientKey.signatureFileName());
		}

		private void createSignatureBlockFile(final Set<ZipEntryAndData> allEntries, final PrivateKey privateKey, final byte[] signatureFileContent, final X509Certificate... signerCertificateChain)
		{
			final SignatureBlockFile file = jarFileHelper.signatureBlockFile(privateKey, signatureFileContent, signerCertificateChain);
			final byte[] signatureBlockFileContent = file.content();
			allEntries.add(new ByteBackedZipEntryAndData(file.signatureBlockFileName(convenientKey), modificationTimeInMillisecondsSinceEpoch, signatureBlockFileContent));
		}
	}

	private class JarFileHelper
	{
		private final JarFile jar;
		private final ZipEntryFilter zipEntryFilter;

		public JarFileHelper(final @NotNull JarFile jar)
		{
			this.jar = jar;
			zipEntryFilter = new ZipEntryFilter(buildLog);
		}

		@NotNull
		public Manifest manifestWithDigests(final @NotNull Set<ZipEntryAndData> allEntries)
		{
			final Manifest jarFileManifest;
			try
			{
				jarFileManifest = jar.getManifest();
			}
			catch (IOException e)
			{
				throw new IllegalStateException(format(UK, "JAR file %1$s does not contain an usable manifest", jar), e);
			}
			final Manifest manifest = (jarFileManifest == null) ? basicManifest(buildLog, buildEnvironment) : jarFileManifest;
			final ManifestAttributesManipulator manipulator = new ManifestAttributesManipulator(manifest, buildLog);
			manipulator.ensureBasicAttributes(buildEnvironment);
			manipulator.putSha1Digests(allEntries);
			return manifest;
		}

		@NotNull
		public Set<ZipEntryAndData> allUsefulEntries()
		{
			return new LinkedHashSet<ZipEntryAndData>()
			{{
				for (ZipEntry zipEntry : zipEntryFilter.usefulZipEntries(jar, new LinkedHashSet<String>()))
				{
					add(new ExistingZipEntryBackedZipEntryAndData(zipEntry, jar));
				}
			}};
		}

		@NotNull
		public SignatureBlockFile signatureBlockFile(final PrivateKey privateKey, final byte[] signatureFileContent, final X509Certificate... signerCertificateChain)
		{
			return new SignatureBlockFile(privateKey, signatureFileContent, signerCertificateChain);
		}
	}
}
